/*
 * // Bristleback plugin - Copyright (c) 2010 bristleback.googlecode.com
 * // ---------------------------------------------------------------------------
 * // This program is free software; you can redistribute it and/or modify it
 * // under the terms of the GNU Lesser General Public License as published by the
 * // Free Software Foundation; either version 3 of the License, or (at your
 * // option) any later version.
 * // This library is distributed in the hope that it will be useful,
 * // but without any warranty; without even the implied warranty of merchantability
 * // or fitness for a particular purpose.
 * // You should have received a copy of the GNU Lesser General Public License along
 * // with this program; if not, see <http://www.gnu.org/licenses/lgpl.html>.
 * // ---------------------------------------------------------------------------
 */

package pl.bristleback.server.bristle.utils;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import pl.bristleback.server.bristle.exceptions.BristleRuntimeException;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Reflection using utility class.
 * <p/>
 * Created on: 2010-11-21 16:34:23 <br/>
 *
 * @author Wojciech Niemiec
 */
public final class ReflectionUtil {
  private static Logger log = Logger.getLogger(ReflectionUtil.class.getName());

  private static final String SETTER_PREFIX = "set";
  private static final String GETTER_PREFIX = "get";
  private static final String BOOL_PREFIX = "is";

  private static final Map<Class, Class> PRIMITIVE_WRAPPERS_MAP = new HashMap<Class, Class>();

  static {
    PRIMITIVE_WRAPPERS_MAP.put(boolean.class, Boolean.class);
    PRIMITIVE_WRAPPERS_MAP.put(char.class, Character.class);
    PRIMITIVE_WRAPPERS_MAP.put(int.class, Integer.class);
    PRIMITIVE_WRAPPERS_MAP.put(long.class, Long.class);
    PRIMITIVE_WRAPPERS_MAP.put(float.class, Float.class);
    PRIMITIVE_WRAPPERS_MAP.put(double.class, Double.class);
  }

  private ReflectionUtil() {
    throw new UnsupportedOperationException();
  }

  public static Getter getGetterMethod(Class ownerClass, String fieldName, boolean includeSuperFields) {
    for (Method method : ownerClass.getMethods()) {
      if (method.getParameterTypes().length > 0) {
        // skip this method because it is not getter
        continue;
      }
      if (!includeSuperFields && isMethodInherited(ownerClass, method)) {
        continue;
      }
      String propertyNameFromMethod = extractFieldNameFromGetter(method);
      if (fieldName.equals(propertyNameFromMethod)) {
        return new Getter(fieldName, method);
      }
    }
    throw new BristleRuntimeException("Getter method for field " + fieldName + " of class " + ownerClass.getSimpleName() + " not found. "
      + "Only public methods with java bean convention are allowed.");
  }

  public static Setter getSetterMethod(Class ownerClass, String fieldName, boolean includeSuperFields) {
    for (Method method : ownerClass.getMethods()) {
      if (!includeSuperFields && isMethodInherited(ownerClass, method)) {
        continue;
      }
      String propertyNameFromMethod = extractFieldNameFromSetter(method);
      if (fieldName.equals(propertyNameFromMethod)) {
        return new Setter(fieldName, method);
      }
    }
    throw new BristleRuntimeException("Setter method for field " + fieldName + " of class " + ownerClass.getSimpleName() + " not found. "
      + "Only public methods with java bean convention are allowed.");
  }

  /**
   * Retrieves and wraps getter method of given class.
   *
   * @param clazz              processed class.
   * @param includeSuperFields if set to true, fields of parent class will be included.
   * @param skippedProperties  properties that should not be included.
   * @return list of getter methods wrapped into {@link pl.bristleback.server.bristle.utils.Getter} objects.
   */
  public static List<Getter> getGetterMethods(Class clazz, boolean includeSuperFields, String... skippedProperties) {
    List<Getter> getters = new ArrayList<Getter>();
    Method[] methods = clazz.getMethods();
    Getter getter;
    for (Method method : methods) {
      if (method.getParameterTypes().length > 0) {
        // skip this method because it is not getter
        continue;
      }
      if (!includeSuperFields && isMethodInherited(clazz, method)) {
        continue;
      }
      String propertyName = extractFieldNameFromGetter(method);
      if (!StringUtils.EMPTY.equals(propertyName)) {
        getter = new Getter(propertyName, method);
        if (!isPropertySkipped(getter, skippedProperties)) {
          getters.add(getter);
        }
      }
    }
    return getters;
  }

  private static boolean isMethodInherited(Class clazz, Method method) {
    return method.getDeclaringClass() != clazz;
  }

  private static boolean isPropertySkipped(Getter getter, String[] skippedProperties) {
    for (String skippedProperty : skippedProperties) {
      if (skippedProperty.equalsIgnoreCase(getter.getFieldName())) {
        return true;
      }
    }
    return false;
  }

  private static String extractFieldNameFromSetter(Method method) {
    String methodName = method.getName();
    return extractFieldName(methodName, SETTER_PREFIX);
  }

  private static String extractFieldNameFromGetter(Method method) {
    String methodName = method.getName();
    if (methodName.equals("getClass")) {
      return StringUtils.EMPTY;
    }
    return extractFieldName(methodName, GETTER_PREFIX, BOOL_PREFIX);
  }

  private static String extractFieldName(String methodName, String... allowedPrefixes) {
    for (String allowedPrefix : allowedPrefixes) {
      if (methodName.startsWith(allowedPrefix)) {
        return methodName.substring(allowedPrefix.length(), allowedPrefix.length() + 1).toLowerCase() + methodName.substring(allowedPrefix.length() + 1);
      }
    }
    return StringUtils.EMPTY;
  }

  public static Field getDependencyFieldForName(Class ownerClass, Class dependencyClass, String dependencyName) {
    try {
      Field field = ownerClass.getDeclaredField(dependencyName);
      if (!field.getType().equals(dependencyClass)) {
        throw new BristleRuntimeException("Field " + dependencyName + " is not of type " + dependencyClass.getName());
      }
      return field;
    } catch (NoSuchFieldException e) {
      throw new BristleRuntimeException("Cannot find dependency with name " + dependencyName, e);
    }
  }

  public static List<Field> getDependencyFieldsWithType(Class ownerClass, Class dependencyClass, boolean includeSuperClass) {
    List<Field> dependencyFields = new ArrayList<Field>();
    Field[] fields = ownerClass.getDeclaredFields();
    for (Field field : fields) {
      if (field.getType().equals(dependencyClass)) {
        dependencyFields.add(field);
      }
    }
    if (includeSuperClass && hasNotBasicSuperclass(ownerClass)) {
      List<Field> superClassFields = getDependencyFieldsWithType(ownerClass.getSuperclass(), dependencyClass, includeSuperClass);
      dependencyFields.addAll(superClassFields);
    }
    return dependencyFields;
  }

  public static boolean hasInterface(Class ownerClass, Class interfaceClass) {
    for (Class interfaceToCheck : ownerClass.getInterfaces()) {
      if (interfaceToCheck.equals(interfaceClass)) {
        return true;
      }
      if (hasNotBasicSuperclass(ownerClass)) {
        return hasInterface(ownerClass.getSuperclass(), interfaceClass);
      }
    }
    return false;
  }

  private static boolean hasNotBasicSuperclass(Class ownerClass) {
    return !ownerClass.getSuperclass().equals(Object.class);
  }

  public static boolean isRawType(Object value) {
    if (value == null) {
      return true;
    }
    Class valueClass = value.getClass();
    return valueClass.isPrimitive()
      || valueClass == Boolean.class
      || valueClass == Integer.class
      || valueClass == Long.class
      || valueClass == Double.class
      || valueClass == Character.class
      || valueClass == String.class
      || valueClass == BigInteger.class
      || valueClass == BigDecimal.class;
  }

  public static Class getWrapperClassForPrimitive(Class primitiveClass) {
    return PRIMITIVE_WRAPPERS_MAP.get(primitiveClass);
  }
}
